#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>

#ifdef _MSC_VER

#ifndef WIN32
#define WIN32
#endif

#include <winsock2.h>
#include <fcntl.h>

#define popen      _popen
#define pclose     _pclose

#define SHUT_RDWR  SD_BOTH
#define SHUT_WR    SD_SEND
#define socklen_t  int
#ifdef errno
#undef errno
#endif
#define errno      GetLastError()

#define PERM_RD    "rb"
#define PERM_WR    "wb"

/* Windows 'strerror()' replacement */
char* strerror(int error)
{
	static char msg[1024];
	char* p = msg;

	FormatMessage(
		FORMAT_MESSAGE_FROM_SYSTEM |
		FORMAT_MESSAGE_IGNORE_INSERTS,
		NULL,
		error,
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
		msg,
		sizeof(msg)-1,
		NULL
	);

	p = msg + strlen(msg);
	
	/* trim bloody CRLFs and '.' */
	for(p = msg + strlen(msg) - 1; p >= msg && (*p == '\n' || *p == '\r' || *p == '.');) 
		*p-- = 0;
	
	return msg;
}

#define socket win32_socket
#define fdopen win32_fdopen

int win32_socket( int domain, int type, int protocol )
{
	SOCKET wsock = WSASocket( domain, type, protocol, NULL, 0, 0 );

	if ( wsock == INVALID_SOCKET )
	{
		wsock = (SOCKET) -1;
	}

	return (int)wsock;
}

FILE* win32_fdopen( int fd, const char* mode )
{
	int translated_fd;
	int osf_flags = 0;
	char osf_mode[8] = "";

	if (!strcmp( mode, PERM_RD))
	{
		osf_flags = _O_RDONLY | _O_BINARY;
		strcpy(osf_mode, "rbc");
	}
	else if(!strcmp( mode, PERM_WR))
	{
		osf_flags = _O_WRONLY | _O_CREAT | _O_TRUNC  | _O_BINARY;
		strcpy(osf_mode, "wbc");
	}

	if ( ( translated_fd = _open_osfhandle( fd, osf_flags ) ) < 0 )
	{
		return NULL;
	}

	return _fdopen( translated_fd, osf_mode );
}

#else

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>

#define PERM_RD    "r"
#define PERM_WR    "w"

#define closesocket close
#define mkdir(path) mkdir(path, 0777)
#endif

const int version = 466;

/* file record */
struct filerec_t {
	char* action;            /* P4 action: edit, add, delete, branch, integrate */
	char* depot_name;        /* //depot/... prefixed file name */ 
	char* local_name;        /* local file system name prefixed by $P4SRCROOT*/
	char* tar_name;          /* relative to $P4SRCROOT path */
	long offset;             /* file body offset in the tar file */
	long sz;                 /* file body length */
	struct filerec_t* prev;  /* next file in the list */
};

/* patcher TCP/IP port */
int default_port = 20050;

/* executable self-name (argv[0]) */
char* exe_name;

/* P4 command */
char p4[4096];

/* patch archive data: file name, file body and length */
char patch_archive_name[1024];
char* patch_archive = 0;
int patch_archive_sz;

char opened_file_list[1024];

char host[1024];
int port;
int listen_port;

const char* list_filename = ".patch.lst";
int num_depots = 0;
char **depot_names = NULL;

char* p4_src_root;

/* if 1, no real action is invloled, only show the action */
int pre_check = 0;

FILE* istream;
FILE* ostream;
FILE* estream;

int _max(int a, int b)
{
	return a > b ? a : b;
}

int _min(int a, int b)
{
	return a < b ? a : b;
}

void usage()
{
	char* p = exe_name + strlen(exe_name);

	/* get file name without path */
	while (p != exe_name && *p != '/' && *p != '\\') {
		*p = tolower(*p);
		--p;
	}
	/* if it points on slash, go to the next position */
	if (p != exe_name) ++p;
	
	fprintf(ostream,
		"P4 Network Patcher  Version %d.%02d\n"
		"usage:\n"
		"%s [diff|patch|revert|server|exec] [-o list] [-a patch_archive] [-p port] [-h host] [-l port] -n {-p4 ...}\n"
		"   diff      - create patch file\n"
		"   patch     - apply patch file\n"
		"   revert    - revert changed files\n"
		"   server    - run patch server\n"
		"   exec      - run P4 command remotely\n"
		"   -o        - patch file list (default: p4 opened)\n"
		"   -a        - patch archive name (default: %s)\n"
		"   -h        - remote host\n"
		"   -p        - remote port (default: %d)\n"
		"   -l        - listen port (default: %d)\n"
		"   -n        - show-only mode without real action\n"
		"   -p4       - pass all other options to p4\n",
		version / 100, version % 100,
		p, patch_archive_name, default_port, default_port
	);
	
	exit(1);
}

void error(const char* fmt, int line, ...)
{
	char buf[10240];
	
	va_list ap;
	va_start(ap, line);
	vsprintf(buf, fmt, ap);
	va_end(ap);
	
	fprintf(estream, "error: %s (line %d, %d, %s)\n", buf, line, errno, strerror(errno));
	
	exit(1);
}

void logic_error(const char* fmt, int line, ...)
{
	char buf[10240];
	
	va_list ap;
	va_start(ap, line);
	vsprintf(buf, fmt, ap);
	va_end(ap);
	
	fprintf(estream, "error: %s (line %d)\n", buf, line);
	
	exit(1);
}

char* normalize_slash(char* s)
{
	char* p;
	#ifdef WIN32
	for (p = s; *p; p++)
		if (*p == '\\')
			*p = '/';
	#endif
	return s;
}

FILE* tar_init(const char* archive)
{
	FILE* f = fopen(archive, PERM_WR);
	
	if (!f) error("tar create", __LINE__);

	return f;
}

void tar_close(FILE* f)
{
	char z[1024];
	
	memset(z, 0, sizeof(z));
	
	if (fwrite(z, 1, sizeof(z), f) != sizeof(z)) error("tar close", __LINE__);
	
	fclose(f);
}

void tar_add(FILE* f, const char* local_name, const char* tar_name, char* in_mem, int in_mem_sz)
{
	char header[512];
	FILE* l;
	long local_sz = local_name ? 0 : in_mem_sz;
	char* local_buf;
	int check_sum = 0;
	int i;
	int tar_name_len;
	
	if (local_name) {
		l = fopen(local_name, PERM_RD);
		if (!l) error("open(%s)", __LINE__, local_name);
		fseek(l, 0L, SEEK_END);
		local_sz = ftell(l);
		rewind(l);
	}
	
	memset(header, 0, sizeof(header));
	memset(header + 148, ' ', 8);
	
	tar_name_len = strlen(tar_name);
	if (tar_name_len <= 100) {
		memcpy(header + 000, tar_name, tar_name_len);
	} else {
		/* break name into a prefix and name */
		int prefix_len;
		char *split_point = strchr(tar_name + tar_name_len - 100, '/');

		if (!split_point || tar_name + 155 < split_point)
			error("Filename too long", __LINE__);

		prefix_len = split_point - tar_name;
		memcpy(header + 000, split_point + 1, tar_name_len - prefix_len - 1);
		memcpy(header + 345, tar_name, prefix_len);
	}
	sprintf(header + 100, "%o", 0666);
	sprintf(header + 124, "%o", local_sz);
	sprintf(header + 257, "%s", "ustar");
	sprintf(header + 263, "%s", "00");

	for (i = 0; i < sizeof(header); i++)
		check_sum += (unsigned char)header[i];
		
	sprintf(header + 148, "%o", check_sum);
	
	/* Write a header */
	if (fwrite(header, 1, sizeof(header), f) != sizeof(header)) 
		error("tar header write", __LINE__);

	if (local_name) {
		while (!feof(l)) {
			char buf[4096];
			int sz = fread(buf, 1, sizeof(buf), l);
			fwrite(buf, 1, sz, f);
		}
	} else {
		fwrite(in_mem, 1, local_sz, f);
	}
	
	/* Write a trailer */
	memset(header, 0, sizeof(header));

	/* Align to the 512 boundary if needed */
	
	if (local_sz % 512) {
		if (fwrite(header, 512 - (local_sz % 512), 1, f) != 1) 
			error("write()", __LINE__);
	}
	
	if (local_name)
		fclose(l);
}

/*
  * parse the list of the opened files by P4  (from "p4 opened" command or from 
  * the explicitly given list) into the memory linked list 
  */
struct filerec_t* p4_opened_files()
{
	struct filerec_t* opened_list = NULL;
	char cmd[1024];
	FILE* f;

	if (!strlen(opened_file_list)) {
		sprintf(cmd, "%s opened", p4);
		fprintf(ostream, "* getting opened P4 files: %s\n", cmd);
		f = popen(cmd, PERM_RD);
		if (!f) error("popen(%s)", __LINE__, cmd);
	} else {
		f = fopen(opened_file_list, PERM_RD);
		if (!f) error("open(%s)", __LINE__, opened_file_list);
	}
	
	while (!feof(f))
	{
		char line[1024];
		char* p, *p2;
		
		struct filerec_t* rec;
		
		if (fgets(line, sizeof(line)-1, f) == NULL) continue;
		
		/* clean trailing CR and LF */
		for(p = line + strlen(line) - 1; p >= line && (*p == '\n' || *p == '\r');) 
			*p-- = 0;

		/* cut the rest of the line starting from '#' char */
		p = strchr(line, '#');
		if (!p) logic_error("no '#' in line: [%s]", __LINE__, line);
		*p = 0;
		
		rec = malloc(sizeof(*rec));
		memset(rec, 0, sizeof(*rec));
		
		rec->depot_name = malloc(strlen(line)+1);
		strcpy(rec->depot_name, line);
		
		p = strchr(p+1, '-');
		if (!p) logic_error("no '-' in line: [%s]", __LINE__, line);
		
		/* point to the file action (after ' - '): edit, add, delete, branch, integrate */
		p += 2;
		p2 = strchr(p, ' ');
		if (!p2)
			logic_error("no ' ' after file action in line: [%s]", 
				__LINE__, line
			);
	
		*p2 = 0;
		
		rec->action = malloc(strlen(p)+1);
		strcpy(rec->action, p);
		
		rec->prev = opened_list;
		opened_list = rec;
	}
	
	!strlen(opened_file_list) ? pclose(f) : fclose(f);
	
	return opened_list;
}

void clear_depot_names()
{
	int i;

	if (!num_depots)
		return;

	for (i = num_depots - 1 ; i >= 0 ; --i) {
		free(depot_names[i]);
		--num_depots;
	}

	free(depot_names);
	depot_names = 0;
}

void initialize_depot_names()
{
	char cmd[1024];
	FILE* f;

	if (num_depots)
		return;

	sprintf(cmd, "%s depots", p4);
	fprintf(ostream, "* getting known P4 depot names: %s\n", cmd);
	f = popen(cmd, PERM_RD);
	if (!f) error("popen(%s)", __LINE__, cmd);
	while (!feof(f))
	{
		const char *depot_prefix = "Depot";
		char line[1024];
		char* p, *p2;
		
		if (fgets(line, sizeof(line)-1, f) == NULL) continue;
		
		/* clean trailing CR and LF */
		for(p = line + strlen(line) - 1; p >= line && (*p == '\n' || *p == '\r');) 
			*p-- = 0;

		if (!memcmp(line, depot_prefix, strlen(depot_prefix))) {
			++num_depots;
			depot_names = realloc(depot_names, num_depots * sizeof(char*));
			p = strchr(line, ' ') + 1;
			if (!p) logic_error("no ' ' in line: [%s]", __LINE__, line);
			p2 = strchr(p, ' ');
			if (!p2) logic_error("no ' ' in line: [%s]", __LINE__, line);
			*p2 = 0;

			/* //depotname/  with 3 slashes + NULL terminator */
			depot_names[num_depots-1] = malloc(p2 - p + 4);
			strcpy(depot_names[num_depots-1], "//");
			strcat(depot_names[num_depots-1], p);
			strcat(depot_names[num_depots-1], "/");
		}
	}

	pclose(f);
}

void expand_depot_names(struct filerec_t* list)
{
	initialize_depot_names();

	while (list) {
		char buf[1024];
		char *p;
		int i;
		for (i = 0 ; i < num_depots ; ++i) {
			if (!memcmp(list->depot_name, depot_names[i], strlen(depot_names[i])))
				break;
		}

		if (i >= num_depots)
			logic_error("unknown depot file name [%s]", __LINE__, list->depot_name);
		
		list->tar_name = malloc(strlen(list->depot_name) - strlen(depot_names[i] + 1));
		strcpy(list->tar_name, list->depot_name + strlen(depot_names[i]));
		
		list->local_name = malloc(strlen(p4_src_root) + 1 + strlen(list->tar_name) + 1);
		strcpy(list->local_name, p4_src_root);
		strcat(list->local_name, "/");
		strcat(list->local_name, list->tar_name);

		normalize_slash(list->local_name);
		
		list = list->prev;
	}

	clear_depot_names();
}

void free_archive_list(struct filerec_t* list)
{
	while (list) {
		struct filerec_t* prev = list->prev;
	
		if (list->action) free(list->action);
		if (list->depot_name) free(list->depot_name);
		if (list->local_name) free(list->local_name);
		if (list->tar_name) free(list->tar_name);
		free(list);
		
		list = prev;
	}
}

void do_diff()
{
	FILE* tar;
	char* list_content;
	int list_content_sz = 100 * 1024;
	struct filerec_t* opened_files, *opened_files_delete;

	fprintf(ostream, "* making diff to [%s]\n", patch_archive_name);

	opened_files = opened_files_delete = p4_opened_files();

	list_content = malloc(list_content_sz);
	if (list_content == NULL) 
		logic_error("unable to allocate %d bytes", __LINE__, list_content_sz);

	*list_content = 0;

	expand_depot_names(opened_files);

	tar = tar_init(patch_archive_name);
	while (opened_files) {
		fprintf(ostream, "%s %s\n", opened_files->depot_name, opened_files->action);
		
		if (strcmp(opened_files->action, "delete"))
			tar_add(tar, opened_files->local_name, opened_files->tar_name, 
				NULL /* from file */, 0 /* from file */
			);

		if (strlen(list_content) + 
		    strlen(opened_files->action) + strlen(opened_files->tar_name) + 1 
		    > list_content_sz - 1) {
			list_content_sz += list_content_sz;
			list_content = realloc(list_content, list_content_sz);
			if (list_content == NULL) 
				logic_error("unable to re-allocate up to %d bytes", __LINE__, list_content_sz);
		}

		sprintf(list_content + strlen(list_content), 
			"%s:%s\n", 
			opened_files->action, opened_files->tar_name
		);

		opened_files = opened_files->prev;
	}

	/* add the list file */
	tar_add(tar, NULL /* from memory */, list_filename, list_content, strlen(list_content));

	tar_close(tar);

	free_archive_list(opened_files_delete);
	free(list_content);
}

/*
  * parse TAR archive into memory linked list
  */
struct filerec_t* parse_tar_archive()
{
	char* tar = patch_archive;
	char* list_content = NULL;
	struct filerec_t* patch_list = NULL;
	struct filerec_t* deleted_files_list = NULL;
	char* action;

	/* go through all data except the last zeroed 1K because this TAR trailer */
	while ((int)(tar - patch_archive) < patch_archive_sz - 1024) {
		struct filerec_t* rec;
		
		rec = malloc(sizeof(*rec));
		memset(rec, 0, sizeof(*rec));
		
		/* item offset in the tar file */
		rec->offset = (long)(tar - patch_archive) + 512;
		
		/* item size */
		if (sscanf(tar + 124, "%lo", &rec->sz) != 1)
			logic_error("no file length at %08lX (%s)", __LINE__, rec->offset - 512, patch_archive_name);

		/* item name (may or may not have a prefix) */
		if (!tar[345]) {
			int name_len = strlen(tar);
			rec->tar_name = malloc(name_len+1);
			memcpy(rec->tar_name, tar, name_len);
			rec->tar_name[name_len] = 0;
		} else {
			int prefix_len = strlen(tar + 345);
			int name_len = strlen(tar);
			rec->tar_name = malloc(prefix_len + name_len + 1 + 1);
			memcpy(rec->tar_name, tar + 345, prefix_len);
			if ( rec->tar_name[prefix_len - 1] != '/') {
				rec->tar_name[prefix_len] = '/';
				++prefix_len;
			}
			memcpy(rec->tar_name + prefix_len, tar, name_len);
			rec->tar_name[prefix_len + name_len] = 0;
		}

		rec->local_name = malloc(strlen(p4_src_root) + 1 + strlen(rec->tar_name) + 1);
		strcpy(rec->local_name, p4_src_root);
		strcat(rec->local_name, "/");
		strcat(rec->local_name, rec->tar_name);
		
		normalize_slash(rec->local_name);
		
		if (!strcmp(tar, list_filename)) {
			list_content = malloc(rec->sz + 1);
			if (tar + 512 + rec->sz > patch_archive + patch_archive_sz)
				error("wrong length of [%s] in archive [%s]", __LINE__, tar, patch_archive);
			memcpy(list_content, tar + 512, rec->sz);
			list_content[rec->sz] = 0;
		}

		/* skip the 512 header, body and align to 512 byte boundary */
		tar += 512 + rec->sz;
		
		if (rec->sz % 512)
			tar += 512 - (rec->sz % 512);
		
		rec->prev = patch_list;
		patch_list = rec;
	}
	
	if (!list_content)
		logic_error("list file [%s] not found in the archive", 
			__LINE__, list_filename
		);
	
	/* go through the list file and fill up file records */
	action = strtok(list_content, "\n\r");
	while (action) {
		struct filerec_t* l;
		char* name;
		int action_sz;
		int name_sz;
		
		name = strchr(action, ':');
		if (!name) continue;
		++name;
		
		action_sz = (int)(name - action - 1);
		name_sz  = strlen(name);
		
		/* find current item from the list file in the global list */
		for (l = patch_list; l; l = l->prev) {
			if (strcmp(l->tar_name, name)) continue;
			l->action = malloc(action_sz + 1);
			memcpy(l->action, action, action_sz);
			l->action[action_sz] = 0;
			break;
		}

		/*  if "deleted" file found, add it to the list */
		if (!memcmp(action, "delete", action_sz)) {
			struct filerec_t* rec = malloc(sizeof(*rec));
			
			memset(rec, 0, sizeof(*rec));
			
			rec->action = malloc(action_sz + 1);
			memcpy(rec->action, action, action_sz);
			rec->action[action_sz] = 0;
			
			rec->local_name = malloc(strlen(p4_src_root) + 1 + name_sz + 1);
			strcpy(rec->local_name, p4_src_root);
			strcat(rec->local_name, "/");
			strcat(rec->local_name, name);
			
			rec->tar_name = malloc(name_sz + 1);
			strcpy(rec->tar_name, name);
			
			normalize_slash(rec->local_name);

			rec->prev = patch_list;
			patch_list = rec;
		}
		
		action = strtok(NULL, "\n\r");
	}
	
	free(list_content);
	
	return patch_list;
}

/*
 * create a full file path
 */
void create_full_path(const char* path)
{
	const char slash =
#ifdef WIN32
		'\\';
#else
		'/';
#endif
	char *p;

	for (p = (char *)path; p = strchr(p, slash); ++p) {
		char c = *p;
		*p = 0;
		mkdir(path);
		*p = c;
	}
}

/*
 * update file smartly preserving the original file list endings 
 */
void update_file(struct filerec_t* rec)
{
	FILE* f;
	char ending[3];
	char* body;
	int i;
	
	/* default line ending is '\n' */
	strcpy(ending, "\n");
	
	/* if target file exists, read first 1K and try to find line ending in it */
	f = fopen(rec->local_name, PERM_RD);
	if (f) {
		char buf[1024];
		
		memset(buf, 0, sizeof(buf));
		
		fread(buf, 1, sizeof(buf)-1, f);
		
		fclose(f);
		
		if (strstr(buf, "\r\n")) 
			strcpy(ending, "\r\n");
	}

	create_full_path(rec->local_name);

	f = fopen(rec->local_name, PERM_WR);
	if (!f) error("open(%s)", __LINE__, rec->local_name);

	body = patch_archive + rec->offset;
	
	for (i = 0; i < rec->sz; i++) {
		if (i < rec->sz-1 && body[i] == '\r' && body[i+1] == '\n') {
			fwrite(ending, 1, strlen(ending), f);
			i++;
		} else if (body[i] == '\r' || body[i] == '\n') {
			fwrite(ending, 1, strlen(ending), f);
		} else {
			fwrite(body + i, 1, 1, f);
		}
	}

	fclose(f);
}

/*
  * execute the external command and grab the output
  */
void execute_cmd(const char* cmd_line)
{
	FILE* o = popen(cmd_line, PERM_RD);
	if (!o) error("popen(%s)", __LINE__, cmd_line);
	
	fprintf(ostream, "* execute: [%s]\n", cmd_line);
	
	while (!feof(o)) {
		char buf[10240];
		int n = fread(buf, 1, sizeof(buf)-1, o);
		if (n < 1) break;
		buf[n] = 0;
		fprintf(ostream, "%s", buf);
	}
	pclose(o);
}

void do_patch()
{
	struct filerec_t* files, *files_delete;
	
	fprintf(ostream, "* patching from [%s]\n", patch_archive_name);
	
	files = files_delete = parse_tar_archive();

	while (files) {
		char cmd[1024];
		
		if (files->action) {
			FILE* o;
			
			if (!strcmp(files->action, "add") || !strcmp(files->action, "branch"))
			{
				fprintf(ostream, "* update file before 'p4 %s': %s\n", 
					files->action, files->local_name
				);
				if (!pre_check) update_file(files);
			}
			
			strcpy(cmd, "p4 ");
			if (!strcmp(files->action, "add")) strcat(cmd, "add");
			else if (!strcmp(files->action, "edit")) strcat(cmd, "edit");
			else if (!strcmp(files->action, "integrate")) strcat(cmd, "edit");
			else if (!strcmp(files->action, "delete")) strcat(cmd, "delete");
			else if (!strcmp(files->action, "branch")) strcat(cmd, "add");
			else 
				logic_error("unknown action '%s'", __LINE__, files->action);
			
			strcat(cmd, " ");
			strcat(cmd, files->local_name);

			fprintf(ostream, "* --> %s\n", files->local_name);
			
			if (!pre_check) execute_cmd(cmd);

			if (!strcmp(files->action, "edit") || 
			    !strcmp(files->action, "integrate"))
			{
				fprintf(ostream, "* update file after 'p4 %s': %s\n", 
					files->action, files->local_name
				);
				if (!pre_check) update_file(files);
			}
		}
		
		files = files->prev;
	}
	
	free_archive_list(files_delete);
}

void do_revert()
{
	struct filerec_t* file_list, *file_list_delete;
	
	fprintf(ostream, "* reverting from [%s]\n", patch_archive_name);
	
	file_list = file_list_delete = parse_tar_archive();

	while (file_list) {
		char cmd[1024];
		
		if (file_list->action) {
			FILE* o;
			strcpy(cmd, "p4 revert ");
			strcat(cmd, file_list->local_name);
			
			fprintf(ostream, "* --> %s\n", file_list->local_name);

			if (!pre_check) execute_cmd(cmd);

			if (!strcmp(file_list->action, "add") || !strcmp(file_list->action, "branch"))
			{
				fprintf(ostream, "* delete after 'p4 %s': %s\n", 
					file_list->action, file_list->local_name
				);
				if (!pre_check) update_file(file_list);
			}
		}
		
		file_list = file_list->prev;
	}
	
	free_archive_list(file_list_delete);
}

void wait_for_shutdown(int sock)
{
	/* wait for the other end to shutdown */
	char buf[1024];
	while (recv(sock, buf, sizeof(buf)-1, 0 /* no flags */) > 0) {
		/* empty loop body */;
	}
}

int do_main(int argc, char** argv);

void do_server()
{
	int listen_socket;
	struct sockaddr_in addr;
	socklen_t addrlen;
	int true = 1;
	int activity = 0;
	int listen_port_value = listen_port;

	if ((listen_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
		error("socket()", __LINE__);
		
	if (setsockopt(listen_socket, SOL_SOCKET, SO_REUSEADDR, (void *)&true, sizeof(true))) 
		error("setsockopt()", __LINE__);
	
	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = inet_addr("0.0.0.0");
	addr.sin_port = htons(listen_port);
	if (bind(listen_socket, (struct sockaddr *)&addr, sizeof(addr)))
		error("bind()", __LINE__);

	if (listen(listen_socket, 10)) error("listen()", __LINE__);
	
	while (1) {
		char* argv[2];
		int sock;
		
		fprintf(ostream, "* listening on port %d\n", listen_port_value);
		addrlen = sizeof(addr);
		sock = accept(listen_socket, (struct sockaddr *)&addr, &addrlen);
		
		if (sock < 0)
			error("accept()", __LINE__);

		fprintf(ostream, "* incoming connection [%04d]\n", ++activity);

		istream = fdopen(sock, PERM_RD);
		if (!istream) error("fdopen()", __LINE__);
		if (setvbuf(istream, NULL, _IONBF, 0) < 0) error("setvbuf(istream)", __LINE__);

		ostream = fdopen(sock, PERM_WR);
		if (!ostream) error("fdopen()", __LINE__);
		if (setvbuf(ostream, NULL, _IONBF, 0) < 0) error("setvbuf(ostream)", __LINE__);
		
		estream = fdopen(sock, PERM_WR);
		if (!estream) error("fdopen()", __LINE__);
		if (setvbuf(estream, NULL, _IONBF, 0) < 0) error("setvbuf(estream)", __LINE__);

		fprintf(ostream, "* remote execution\n");
		
		argv[0] = exe_name;	argv[1] = "-";

		do_main(2, argv);

		/* make sure that all data in the channel is delivered */
		shutdown(sock, SHUT_WR);
		wait_for_shutdown(sock);
		closesocket(sock);
		
		/* close redirected stream */
		fclose(istream);
		fclose(ostream);
		fclose(estream);

		closesocket(sock);
		
		istream = stdin; ostream = stdout; estream = stderr;
	}
}

int connect_to_remote_host(const char* host, int port)
{
	int sock;
	struct sockaddr_in addr;
	socklen_t addrlen;
	struct hostent* resolver;

	if ((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) error("socket()", __LINE__);
	
	resolver = gethostbyname(host);
	if (!resolver) error("gethostbyname(%s)", __LINE__, host); 
	
	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = ((struct in_addr*)resolver->h_addr)->s_addr;
	addr.sin_port = htons(port);
	if (connect(sock, (struct sockaddr *)&addr, sizeof(addr))) 
		error("connect(%s:%d)", __LINE__, host, port);
	
	return sock;
}

void socket_pass_through(int sock)
{
	char buf[1024];
	
	while (1) {
		int n = recv(sock, buf, sizeof(buf)-1, 0 /* no flags */);
		if (n < 0) error("recv()", __LINE__);
		if (n == 0) break;
		buf[n] = 0;
		
		fprintf(ostream, "%s", buf);
	}
}

int do_main(int argc, char* argv[])
{
	char cmd[1024];

	/* save the executable name */
	exe_name = argv[0];
	
	strcpy(p4, "p4");
	strcpy(patch_archive_name, "patch.tar");
	
	strcpy(host, "");
	port = default_port;
	listen_port = default_port;

	strcpy(opened_file_list, "");
	
	/* if the command is not given, stop it */
	if (argc < 2) usage();
	
	strcpy(cmd, argv[1]);

	if (!strcmp(cmd, "-?")) usage();

	p4_src_root = getenv("P4SRCROOT");
	if (p4_src_root == NULL || strlen(p4_src_root) == 0)
		logic_error("P4SRCROOT variable is not set or empty", __LINE__);

	fprintf(ostream, "* P4SRCROOT=%s\n", p4_src_root);

	if (strcmp(cmd, "-")) {
		/* skip the executable name and the command */
		argv += 2;
		
		while (*argv) {
			if (!strcmp(*argv, "-a") && *(argv+1))
				strcpy(patch_archive_name, *(++argv));
			else if (!strcmp(*argv, "-p") && *(argv+1))
				port = atoi(*(++argv));
			else if (!strcmp(*argv, "-l") && *(argv+1))
				listen_port = atoi(*(++argv));
			else if (!strcmp(*argv, "-?"))
				usage();
			else if (!strcmp(*argv, "-n") && *(argv+1))
				pre_check = 1;
			else if (!strcmp(*argv, "-h") && *(argv+1))
				strcpy(host, *(++argv));
			else if (!strcmp(*argv, "-o") && *(argv+1))
				strcpy(opened_file_list, *(++argv));
			else if (!strcmp(*argv, "-p4")) {
				while (*(++argv)) {
					strcat(p4, " ");
					strcat(p4, *argv);
				}
				break;
			} else
				logic_error("error: unknown '%s'", __LINE__, *argv);
			++argv;
		}
	} else {
		char line[1024];
		char* p;
		int sz;

		/* read the command: patch, revert, exec */
		if (!fgets(line, sizeof(line)-1, istream)) error("fgets()", __LINE__);
		sz = strlen(line);
		
		/* trim trailing CR and LF */
		for(p = line + sz - 1; p >= line && (*p == '\n' || *p == '\r');) *p-- = 0;

		strcpy(cmd, line);
		
		if (!fgets(line, sizeof(line)-1, istream)) error("fgets()", __LINE__);
		sz = strlen(line);
		
		/* trim trailing CR and LF */
		for(p = line + sz - 1; p >= line && (*p == '\n' || *p == '\r');) *p-- = 0;
		
		patch_archive_sz = atoi(line);

		patch_archive = malloc(patch_archive_sz);

		sz = 0;
		while (sz < patch_archive_sz) {
			int n = fread(patch_archive + sz, 1, _min(patch_archive_sz - sz, 1024), istream);
			if (n < 0) error("read(istream)", __LINE__);
			sz += n;
		}
		
		strcpy(patch_archive_name, "standard input");
		
		if (!strcmp(cmd, "exec")) {
			memcpy(p4, patch_archive, patch_archive_sz);
			p4[patch_archive_sz] = 0;
		}
	}

	if (pre_check)
		fprintf(ostream, "* show only mode, no real action is performed\n");
		
	if (!strcmp(cmd, "diff"))
		do_diff();
	else if (!strcmp(cmd, "patch") || !strcmp(cmd, "revert")) {
	
		if (!patch_archive) {
		
			FILE* f;
			int sz;
			
			f = fopen(patch_archive_name, PERM_RD);
			if (!f) error("open(%s)", __LINE__, patch_archive_name);
			
			fseek(f, 0L, SEEK_END);
			patch_archive_sz = ftell(f);

			patch_archive = malloc(patch_archive_sz);
		
			rewind(f);

			sz = 0;
			while (sz < patch_archive_sz) {
				int n = fread(patch_archive + sz, 1, _min(patch_archive_sz - sz, 10240), f);
				if (n < 0) error("read(%s)", __LINE__, patch_archive_name);
				
				sz += n;
			}
			
			fclose(f);
				
			fprintf(ostream, "* archive loaded: %s, length %d\n", 
				patch_archive_name, patch_archive_sz
			);
		}

		if (strlen(host)) {
			char cmd_line[1024];
			int sock;
			int i;
			
			/* host is given than execute remotely */
			
			fprintf(ostream, "* execute remote %s\n", cmd);
			
			sock = connect_to_remote_host(host, port);

			sprintf(cmd_line, "%s\n%d\n", cmd, patch_archive_sz);

			if (send(sock, cmd_line, strlen(cmd_line), 0 /* no flags */) < 0)
				error("send(%s)", __LINE__, cmd_line);
			
			i = 0;
			while (i < patch_archive_sz) {
				int n = send(sock, patch_archive + i, _min(patch_archive_sz - i, 1024), 0 /* no flags */);
				if (n < 0) error("send()", __LINE__);
				i += n;
			}

			fprintf(ostream, "* file %s (%d byte(s)) is sent to the remote machine\n",
				patch_archive_name, i
			);

			fprintf(ostream, "* all lines below are from the remote machine\n");
			socket_pass_through(sock);
			
			shutdown(sock, SHUT_RDWR);
			closesocket(sock);
			
		} else {
			/* local mode */
			fprintf(ostream, "* execute local %s\n", cmd);
			
			strcmp(cmd, "patch") ? do_revert() : do_patch();
		}

		free(patch_archive);
		
	} else if (!strcmp(cmd, "server"))
		do_server();
	else if (!strcmp(cmd, "exec")) {
		if (strlen(host)) {
			char cmd_line[1024];
		
			int sock = connect_to_remote_host(host, port);
			
			sprintf(cmd_line, "%s\n%d\n%s", cmd, strlen(p4), p4);

			if (send(sock, cmd_line, strlen(cmd_line), 0 /* no flags */) < 0)
				error("send(%s)", __LINE__, cmd_line);
			
			socket_pass_through(sock);
			
			shutdown(sock, SHUT_RDWR);
			closesocket(sock);
			
		} else
			execute_cmd(p4);
	} else 
		logic_error("unknown command '%s'", __LINE__, cmd);

	return 0;
}

int main(int argc, char* argv[])
{
	int exitcode;
	
	#ifdef _MSC_VER
	WORD wVersionRequested = MAKEWORD(2, 2);
	WSADATA wsaData;
	int ws_error;

	ws_error = WSAStartup(wVersionRequested, &wsaData);
	if (ws_error != 0) error("WSAStartup() = %d", __LINE__, ws_error);
	#endif
	
	istream = stdin;
	ostream = stdout;
	estream = stderr;
	
	exitcode = do_main(argc, argv);

	#ifdef _MSC_VER
	WSACleanup();
	#endif

	return exitcode;
}
